<?php
/**
 * コーディング規約
 *
 * @author admin
 *
 */
class Cs
{
	/** 関数の行数限界値 */
	const MAX_LINE_COUNT_FUNCTION = 40;
	/** トークン一覧 */
	protected $_arrTokenList;
	/** トークン数 */
	protected $_iTokenCount;
	/** エラーメッセージリスト */
	protected $_arrErrMessageList;
	/** トークン関数戻り値バッファ */
	protected $_arrTokenBuff;

	/**
	 * コンストラクタ
	 *
	 * @param string $sSrcText ソース
	 */
	public function __construct($sSrcText)
	{
		$arrTokenBuff = token_get_all( $sSrcText );
		$this->_arrTokenBuff = $arrTokenBuff;
		$this->_arrTokenList = $this->_unsetWhiteSpaceToken( $arrTokenBuff );
		$this->_iTokenCount = count($this->_arrTokenList);
		$this->_arrErrMessageList = array();
	}

	/**
	 * コードチェック
	 */
	public function check()
	{
		$i = 0;
		/** 先頭はＰＨＰオープンタグであること */
		if ( $this->_getTokenKey(0) == T_OPEN_TAG ) {
			$i ++;
		} else {
			$this->_arrErrMessageList[]
			= CsMessage::createMessage(
				CsMessage::MSG_ERR_PHPSTART,
				$this->_getTokenLineNum($i));
		}
		/** 次はファイルコメントドキュメントであること */
		if ( $this->_getTokenKey($i) == T_DOC_COMMENT ) {
			$i ++;
		} else {
			$this->_arrErrMessageList[]
			= CsMessage::createMessage(
				CsMessage::MSG_ERR_FILEDOC,
				$this->_getTokenLineNum($i));
		}

		while ( $i < $this->_iTokenCount ) {
			$sTokenKey = $this->_getTokenKey($i);
			/**
			 * 必須ブロックドキュメントコメントチェック
			 */
			switch ($sTokenKey) {
				case T_CLASS:
//				case T_PROTECTED:
//				case T_PRIVATE:
//				case T_PUBLIC:
//				case T_CONST:
				case T_IF:
				case T_WHILE:
				case T_FOREACH:
				case T_FOR:
				case T_STATIC:
					$sPrevTokenKey = $this->_prevTokenKey($i);
					if (
						($sPrevTokenKey != T_DOC_COMMENT)
						&&
						($sPrevTokenKey != T_COMMENT)
						&&
						($sPrevTokenKey != T_ELSE)
						&&
						($sPrevTokenKey != T_STATIC)
					)
					{
						$sErrMsg = CsMessage::MSG_ERR_DOC;
						switch ($sTokenKey) {
							case T_CLASS:
								$sErrMsg = CsMessage::MSG_ERR_CLASSDOC;
								break;
							case T_PROTECTED:
							case T_PRIVATE:
							case T_PUBLIC:
							case T_STATIC:
								$sErrMsg = CsMessage::MSG_ERR_VARDOC;
								break;
							case T_CONST:
								$sErrMsg = CsMessage::MSG_ERR_CONSTDOC;
								break;
							case T_IF:
								$sErrMsg = CsMessage::MSG_ERR_CONDITIONDOC;
								break;
							case T_WHILE:
							case T_FOREACH:
							case T_FOR:
								$sErrMsg = CsMessage::MSG_ERR_LOOPDOC;
								break;
						}
						$this->_arrErrMessageList[]
						= CsMessage::createMessage(
							$sErrMsg,
							$this->_getTokenLineNum($i));
					}
					break;
			}

			/**
			 * プライベートプロテクト命名規則チェック
			 */
			switch ($sTokenKey) {
				case T_PROTECTED:
				case T_PRIVATE:
					$iLineNum = $this->_getTokenLineNum($i);
					$sName = $this->_getTokenValueInLine(T_VARIABLE, $iLineNum);
					/** ＦＡＮＣＴＩＯＮ */
					if ( $this->_TokenNameExistInLine(T_FUNCTION, $iLineNum) == TRUE ) {
						$sName = $this->_getTokenValueInLine(T_STRING, $iLineNum);
						if ( $sName !== FALSE ) {
/*
							if ( substr($sName, 0, 1) != '_' ) {
								$this->_arrErrMessageList[]
								= CsMessage::createMessage(
									CsMessage::MSG_ERR_USC,
									$this->_getTokenLineNum($i));
							}
*/
						}
					} else {
						/** プライベートプロテクト変数 */
						if ( $sName !== FALSE ) {
/*
							if ( substr($sName, 0,2) != '$_') {
								$this->_arrErrMessageList[]
								= CsMessage::createMessage(
									CsMessage::MSG_ERR_USC,
									$this->_getTokenLineNum($i));
							}
*/
						}
					}
					break;
			}

			$i ++;
		}
		$this->check2();
	}

	/**
	 * コードチェックその2
	 */
	public function check2()
	{
		$tokenCount = count($this->_arrTokenBuff);
		$i = 0;
		while ($i < $tokenCount) {
			if ($this->_arrTokenBuff[$i][0] === T_FUNCTION) {
				/*** 関数の行数チェック */
				$lineCount = $this->_getLineCountFunction($i);
				if ($lineCount > self::MAX_LINE_COUNT_FUNCTION) {
					$this->_arrErrMessageList[]
					= CsMessage::createMessage(
							CsMessage::MSG_ERR_FUNCTION_LINECOUNTOVER,
							$this->_arrTokenBuff[$i][2]);
				}
			}
			$i ++;
		}
	}

	/**
	 * 関数の行数を返す
	 * @param $i 関数宣言を検出した行番号
	 */
	protected function _getLineCountFunction($i)
	{
		$tokenCount = count($this->_arrTokenBuff);
		/*** 関数開始行保存 */
		$startFunctionLineNum = $this->_arrTokenBuff[$i][2];
		/*** 中括弧開始行走査 */
		$i = $this->getBraceStartLineIndex($i);
		/*** 見つからなければゼロ行リターン */
		if ($i == $tokenCount) {
			return 0;
		}
		/*** 関数最終トークン行番号用行番号初期化 */
		$lastTokenLineNum = $i;
		/** 中括弧カウンタ初期化 */
		$braceCounter = 0;
		/*** 関数終了中括弧走査 */
		while ($i < $tokenCount) {
			/*** 中括弧ネスト開始：中括弧カウンタ1アップ */
			if ($this->_arrTokenBuff[$i] === '{') {
				$braceCounter ++;
			}
			/*** 中括弧ネスト終了：中括弧カウンタ１ダウン */
			else if ($this->_arrTokenBuff[$i] === '}') {
				$braceCounter --;
			}
			else
			{
				/*** 関数最終トークン行番号用行番号保存 */
				if (isset($this->_arrTokenBuff[$i][2])) {
					$lastTokenLineNum = $this->_arrTokenBuff[$i][2];
				}
			}
			/*** 関数終了判定 */
			if ($braceCounter == 0) {
				break;
			}
			$i ++;
		}
		/*** 見つからなければゼロ行リターン */
		if ($i == $tokenCount) {
			return 0;
		}
		return $lastTokenLineNum - $startFunctionLineNum;
	}

	/**
	 * 中括弧開始のインデックスを返す
	 * @param $i 捜査を開始するインデックス
	 */
	public function getBraceStartLineIndex($i = 0)
	{
		$tokenCount = count($this->_arrTokenBuff);
		/*** 中括弧開始行走査 */
		while ($i < $tokenCount) {
			if ($this->_arrTokenBuff[$i] === '{') {
				break;
			}
			$i ++;
		}
		return $i;
	}

	/**
	 * エラーメッセージリストを返す
	 *
	 */
	public function getErrMessageList()
	{
		return $this->_arrErrMessageList;
	}

	/**
	 * 指定行に指定トークンがあればその値を返す
	 *
	 * @param  $sToken
	 * @param  $iLineNum
	 * @return  string|boolern
	 */
	protected function _getTokenValueInLine($sToken, $iLineNum)
	{
		$sResult = '';
		if ( $this->_TokenNameExistInLine($sToken, $iLineNum) == FALSE ) {
			return FALSE;
		}
		$arrTokenList = $this->_getTokenListSameLine($iLineNum);
		foreach ( $arrTokenList as $arrToken ) {
			if ( $arrToken[0] == $sToken ) {
				$sResult = $arrToken[1];
				break;
			}
		}
		return $sResult;
	}

	/**
	 * 指定トークンが指定行にあればＴＲＵＥを返す
	 *
	 * @param $sToken
	 * @param $iLineNum
	 */
	protected function _TokenNameExistInLine( $sToken, $iLineNum )
	{
		$bResult = FALSE;
		$arrTokenNameList = $this->_getTokenNameListSameLine($iLineNum);
		if ( array_search($sToken, $arrTokenNameList) !== FALSE ) {
			$bResult = TRUE;
		}
		return $bResult;
	}

	/**
	 * 指定行にあるトークン識別リストを返す
	 *
	 * @param $iLineNum 行番号
	 * @return String
	 */
	protected function _getTokenNameListSameLine($iLineNum)
	{
		$arrTokenList = $this->_getTokenListSameLine($iLineNum);
		$arrResult = array();
		foreach ($arrTokenList as $arrToken) {
			$arrResult[] = $arrToken[0];
		}
		return $arrResult;
	}

	/**
	 * 指定行のトークンリストを返す
	 *
	 * @param $iLineNum 行番号
	 * @return 行番号
	 */
	protected function _getTokenListSameLine($iLineNum)
	{
		$arrResult = array();
		$i = 0;
		while ( $i < $this->_iTokenCount ) {
			if ( $this->_getTokenLineNum($i) == $iLineNum ) {
				break;
			}
			$i ++;
		}

		while ( $i < $this->_iTokenCount ) {
			if ( $this->_getTokenLineNum($i) != $iLineNum ) {
				break;
			}
			$arrResult[] = $this->_arrTokenList[$i];
			$i ++;
		}
		return $arrResult;
	}

	/**
	 * 直前のトークン識別を返す
	 *
	 * @param unknown_type $index
	 */
	protected function _prevTokenKey( $i ) {
		if ( $i == 0 ) {
			return '';
		}
		$sResult = $this->_getTokenKey($i-1);
		return $sResult;
	}

	/**
	 * トークンリストからホワイトスペースを削除して返す
	 *
	 * @param array $arrTokenList トークンリスト
	 * @return array トークンリスト
	 */
	protected function _unsetWhiteSpaceToken( $arrTokenList )
	{
		$arrResult = array();
		$iTokenCount = count( $arrTokenList );
		$i = 0;
		while ( $i < $iTokenCount ) {
			if ( is_array($arrTokenList[$i]) == FALSE ) {
				unset($arrTokenList[$i]);
			} else if ( $arrTokenList[$i][0] ==  T_WHITESPACE ) {
				unset( $arrTokenList[$i] );
			} else {
				$arrTokenList[$i]['T_NAME'] = token_name($arrTokenList[$i][0]);
			}
			$i ++;
		}
		$arrTokenList = array_merge($arrTokenList);
		$arrResult = $arrTokenList;

		return $arrResult;
	}

	/**
	 * トークン識別コードを返す
	 *
	 * @param $i
	 */
	protected function _getTokenKey( $i )
	{
		$sResult = '';
		if ( is_array($this->_arrTokenList[$i]) ) {
			$sResult = $this->_arrTokenList[$i][0];
		}
		return $sResult;
	}

	/**
	 * トークンの出現行番号を返す
	 *
	 * @param array $arrToken
	 * @return integer
	 */
	protected function _getTokenLineNum( $i )
	{
		$iResult = $this->_arrTokenList[$i][2];
		return $iResult;
	}
}

class CsMessage
{
	const MSG_ERR_PHPSTART = 'ERR(%s) 先頭行に [<?php] が見つかりません';
	const MSG_ERR_FILEDOC = 'ERR(%s) ファイルコメントの記載がありません';
	const MSG_ERR_CLASSDOC = 'ERR(%s) クラスコメントの記載がありません';
	const MSG_ERR_VARDOC = 'ERR(%s) 変数 | 関数コメントの記載がありません';
	const MSG_ERR_CONSTDOC = 'ERR(%s)定数コメントの記載がありません';
	const MSG_ERR_DOC = 'ERR(%s) コメントを記載してください';
	const MSG_ERR_USC = 'ERR(%s) protected | private 変数|関数 名はアンダースコアからはじまります';
	const MSG_ERR_CONDITIONDOC = 'ERR(%s) 条件分岐コメントの記載がありません。';
	const MSG_ERR_LOOPDOC = 'ERR(%s) ループコメントの記載がありません';
	const MSG_ERR_FUNCTION_LINECOUNTOVER = 'ERR(%s) 関数が40行を越えています';

	public static function createMessage($sMessage, $iLineNum)
	{
		$sResult = sprintf($sMessage,$iLineNum);
		return $sResult;
	}
}

//echo $argc."\r\n";
echo "PHP CODE CHECKER version 1.2 \r\n";
echo date(DATE_RSS)."\r\n";
//print_r( $argv );
if ( !isset($argv[1] ) ) {
	echo "ファイルを指定してください。\r\n";
	exit;
}
$filename = $argv[1];
if ( !file_exists($filename) ) {
	echo "OPEN ERR " . $filename . "\r\n";
	exit;
}
$sSrcText = file_get_contents($filename);
echo "source file -- " . $filename . "\r\n";
echo "-- process start --\r\n";
$objCs = new Cs($sSrcText);
$objCs->check();
$arrErrMessageList = $objCs->getErrMessageList();
foreach ( $arrErrMessageList as $sErrMessage ) {
	echo $sErrMessage ."\r\n";
}
echo "error count " . count($arrErrMessageList) . "\r\n";
echo "-- process end --\r\n";